/*
 * SoapUI, Copyright (C) 2004-2017 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent 
 * versions of the EUPL (the "Licence"); 
 * You may not use this work except in compliance with the Licence. 
 * You may obtain a copy of the Licence at: 
 * 
 * http://ec.europa.eu/idabc/eupl 
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is 
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
 * express or implied. See the Licence for the specific language governing permissions and limitations 
 * under the Licence. 
 */

package com.eviware.soapui.impl.wsdl.mock.dispatch;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.config.MockOperationQueryMatchDispatchConfig;
import com.eviware.soapui.impl.support.actions.ShowOnlineHelpAction;
import com.eviware.soapui.impl.wsdl.mock.DispatchException;
import com.eviware.soapui.impl.wsdl.mock.WsdlMockOperation;
import com.eviware.soapui.impl.wsdl.mock.WsdlMockResponse;
import com.eviware.soapui.impl.wsdl.mock.WsdlMockRunContext;
import com.eviware.soapui.impl.wsdl.support.HelpUrls;
import com.eviware.soapui.model.mock.MockOperation;
import com.eviware.soapui.model.mock.MockRequest;
import com.eviware.soapui.model.mock.MockResponse;
import com.eviware.soapui.model.mock.MockResult;
import com.eviware.soapui.model.propertyexpansion.PropertyExpander;
import com.eviware.soapui.support.AbstractPropertyChangeNotifier;
import com.eviware.soapui.support.StringUtils;
import com.eviware.soapui.support.UISupport;
import com.eviware.soapui.support.components.JXToolBar;
import com.eviware.soapui.support.components.SimpleBindingForm;
import com.eviware.soapui.support.xml.XmlUtils;
import com.eviware.soapui.ui.support.ModelItemDesktopPanel;
import com.jgoodies.binding.PresentationModel;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlObject;

import javax.swing.AbstractAction;
import javax.swing.AbstractListModel;
import javax.swing.Action;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class QueryMatchMockOperationDispatcher extends AbstractMockOperationDispatcher implements
        PropertyChangeListener {
    private MockOperationQueryMatchDispatchConfig conf;
    private List<Query> queries = new ArrayList<Query>();
    private PresentationModel<Query> queryDetailFormPresentationModel;
    private QueryItemListModel queryItemListModel;
    private JList itemList;
    private JButton deleteButton;
    private JButton copyButton;
    private JButton renameButton;
    private SimpleBindingForm detailForm;
    private JButton runButton;
    private JButton declareNsButton = new JButton(new DeclareNamespacesAction());
    private JButton extractFromCurrentButton = new JButton(new ExtractFromCurrentAction());

    public QueryMatchMockOperationDispatcher(MockOperation mockOperation) {
        super(mockOperation);

        try {
            conf = MockOperationQueryMatchDispatchConfig.Factory.parse(((WsdlMockOperation) mockOperation).getConfig().getDispatchConfig().xmlText());

            for (MockOperationQueryMatchDispatchConfig.Query query : conf.getQueryList()) {
                queries.add(new Query(query));
            }
        } catch (XmlException e) {
            e.printStackTrace();
        }

        mockOperation.addPropertyChangeListener("mockResponses", this);
    }

    @Override
    public void release() {
        getMockOperation().removePropertyChangeListener("mockResponses", this);
        super.release();
    }

    @Override
    public JComponent getEditorComponent() {
        JSplitPane splitPane = UISupport.createHorizontalSplit(buildQueryListComponent(), buildQueryDetailComponent());
        splitPane.setDividerLocation(300);
        setEnabled();
        return splitPane;
    }

    @Override
    public boolean hasDefaultResponse() {
        return true;
    }

    protected Component buildQueryListComponent() {
        JPanel panel = new JPanel(new BorderLayout());

        queryItemListModel = new QueryItemListModel();
        itemList = new JList(queryItemListModel);
        itemList.setCellRenderer(new QueryItemListCellRenderer());
        itemList.addListSelectionListener(new ListSelectionListener() {
            public void valueChanged(ListSelectionEvent e) {
                queryDetailFormPresentationModel.setBean((Query) itemList.getSelectedValue());
                setEnabled();
            }
        });

        panel.add(buildItemsToolbar(), BorderLayout.NORTH);
        panel.add(new JScrollPane(itemList), BorderLayout.CENTER);

        return panel;
    }

    protected void setEnabled() {
        QueryMatchMockOperationDispatcher.Query bean = queryDetailFormPresentationModel.getBean();

        detailForm.setEnabled(bean != null);
        renameButton.setEnabled(bean != null);
        deleteButton.setEnabled(bean != null);
        copyButton.setEnabled(bean != null);
        extractFromCurrentButton.setEnabled(bean != null);
        copyButton.setEnabled(bean != null);
        declareNsButton.setEnabled(bean != null);
        runButton.setEnabled(getQueryCount() > 0);
    }

    private JXToolBar buildItemsToolbar() {
        JXToolBar toolbar = UISupport.createSmallToolbar();

        runButton = UISupport.createToolbarButton(new RunAction());
        toolbar.addFixed(runButton);
        toolbar.addSeparator();

        toolbar.addFixed(UISupport.createToolbarButton(new AddAction()));
        deleteButton = UISupport.createToolbarButton(new DeleteAction());
        deleteButton.setEnabled(false);
        toolbar.addFixed(deleteButton);
        toolbar.addSeparator();
        copyButton = UISupport.createToolbarButton(new CopyAction());
        copyButton.setEnabled(false);
        toolbar.addFixed(copyButton);
        renameButton = UISupport.createToolbarButton(new RenameAction());
        renameButton.setEnabled(false);
        toolbar.addFixed(renameButton);

        toolbar.addSeparator();

        return toolbar;
    }

    protected Component buildQueryDetailComponent() {
        queryDetailFormPresentationModel = new PresentationModel<Query>(null);
        detailForm = new SimpleBindingForm(queryDetailFormPresentationModel);

        detailForm.setDefaultTextAreaRows(5);
        detailForm.setDefaultTextAreaColumns(50);

        detailForm.append(buildQueryToolbar());
        detailForm.appendTextArea("query", "XPath", "The XPath to query in the request");
        detailForm.appendTextArea("match", "Expected Value", "The value to match");
        JComboBox comboBox = detailForm.appendComboBox("response", "Dispatch to", new MockResponsesComboBoxModel(),
                "The MockResponse to dispatch to");
        UISupport.setFixedSize(comboBox, 150, 20);
        detailForm.appendCheckBox("disabled", "Disabled", "Disables this Query");

        return new JScrollPane(detailForm.getPanel());
    }

    protected JXToolBar buildQueryToolbar() {
        JXToolBar toolBar = UISupport.createSmallToolbar();

        addQueryToolbarActions(toolBar);

        toolBar.addGlue();
        toolBar.addFixed(ModelItemDesktopPanel.createActionButton(new ShowOnlineHelpAction(
                HelpUrls.MOCKOPERATION_QUERYMATCHDISPATCH_HELP_URL), true));

        return toolBar;
    }

    protected void addQueryToolbarActions(JXToolBar toolBar) {
        toolBar.addFixed(declareNsButton);
        toolBar.addFixed(extractFromCurrentButton);
    }

    public WsdlMockResponse selectMockResponse(MockRequest request, MockResult result)
            throws DispatchException {
        Map<String, XmlCursor> cursorCache = new HashMap<String, XmlCursor>();

        try {
            XmlObject xmlObject = request.getRequestXmlObject();

            for (Query query : getQueries()) {
                if (query.isDisabled()) {
                    continue;
                }

                String path = PropertyExpander.expandProperties(request.getContext(), query.getQuery());
                if (StringUtils.hasContent(path)) {
                    XmlCursor cursor = cursorCache.get(path);
                    if (cursor == null && !cursorCache.containsKey(path)) {
                        cursor = xmlObject.newCursor();
                        cursor.selectPath(path);
                        if (!cursor.toNextSelection()) {
                            cursor.dispose();
                            cursor = null;
                        }
                    }

                    if (cursor != null) {
                        String value = PropertyExpander.expandProperties(request.getContext(), query.getMatch());

                        if (value.equals(XmlUtils.getValueForMatch(cursor))) {
                            WsdlMockRunContext requestContext = (WsdlMockRunContext) request.getRequestContext();
                            requestContext.put("usedQueryMatch", query.getName());

                            WsdlMockResponse resp = null;
                            for (MockResponse mockResponse : this.getMockOperation().getMockResponses()) {
                                if (query.getResponse().equals(mockResponse.getName())) {
                                    resp = (WsdlMockResponse) mockResponse;
                                }
                            }
                            // return getMockOperation().getMockResponseByName(
                            // query.getResponse() );
                            if (resp != null) {
                                return resp;
                            }

                            return ((WsdlMockOperation) getMockOperation()).getMockResponseByName(query.getResponse());
                        }
                    }

                    cursorCache.put(path, cursor);
                }
            }

            return null;
        } catch (Throwable e) {
            throw new DispatchException(e);
        } finally {
            for (XmlCursor cursor : cursorCache.values()) {
                if (cursor != null) {
                    cursor.dispose();
                }
            }
        }
    }

    public Query addQuery(String name) {
        Query query = new Query(conf.addNewQuery());
        query.setName(name);
        queries.add(query);

        getPropertyChangeSupport().firePropertyChange("queries", null, query);

        if (queryItemListModel != null) {
            queryItemListModel.fireAdded();
        }

        return query;
    }

    public void deleteQuery(Query query) {
        int ix = queries.indexOf(query);
        queries.remove(ix);
        getPropertyChangeSupport().firePropertyChange("queries", query, null);

        if (queryItemListModel != null) {
            queryItemListModel.fireRemoved(ix);
        }

        conf.removeQuery(ix);
        saveConfig();
    }

    public Query[] getQueries() {
        return queries.toArray(new Query[queries.size()]);
    }

    public int getQueryCount() {
        return queries.size();
    }

    public Query getQueryByName(String name) {
        for (Query q : queries) {
            if (q.getName().equals(name)) {
                return q;
            }
        }

        return null;
    }

    public Query getQueryAt(int index) {
        return queries.get(index);
    }

    public void propertyChange(PropertyChangeEvent evt) {
        if (queryItemListModel != null) {
            queryItemListModel.refresh();
        }
    }

    public static class Factory implements MockOperationDispatchFactory {
        public MockOperationDispatcher build(MockOperation mockOperation) {
            return new QueryMatchMockOperationDispatcher(mockOperation);
        }
    }

    public class Query extends AbstractPropertyChangeNotifier {
        private MockOperationQueryMatchDispatchConfig.Query config;

        protected Query(MockOperationQueryMatchDispatchConfig.Query config) {
            this.config = config;
        }

        public String getName() {
            return config.getName();
        }

        public void setName(String s) {
            String old = config.getName();
            config.setName(s);
            saveConfig();
            firePropertyChange("name", old, s);
        }

        public boolean isDisabled() {
            return config.getDisabled();
        }

        public void setDisabled(boolean disabled) {
            boolean old = config.getDisabled();
            if (old == disabled) {
                return;
            }
            config.setDisabled(disabled);
            saveConfig();
            firePropertyChange("disabled", old, disabled);
            queryItemListModel.refresh();
        }

        public String getQuery() {
            return config.getQuery();
        }

        public void setQuery(String s) {
            String old = config.getQuery();
            config.setQuery(s);
            saveConfig();
            firePropertyChange("query", old, s);
        }

        public String getMatch() {
            return config.getMatch();
        }

        public void setMatch(String s) {
            String old = config.getMatch();
            config.setMatch(s);
            saveConfig();
            firePropertyChange("match", old, s);
        }

        public String getResponse() {
            return config.getResponse();
        }

        public void setResponse(String s) {
            String old = config.getResponse();
            config.setResponse(s);
            saveConfig();
            firePropertyChange("response", old, s);
        }
    }

    private void saveConfig() {
        ((WsdlMockOperation) getMockOperation()).getConfig().getDispatchConfig().set(conf);
    }

    private class QueryItemListModel extends AbstractListModel {
        public int getSize() {
            return getQueryCount();
        }

        public Object getElementAt(int index) {
            return getQueryAt(index);
        }

        public void refresh() {
            fireContentsChanged(this, 0, getQueryCount());
        }

        public void fireAdded() {
            fireIntervalAdded(this, getQueryCount(), getQueryCount());
        }

        public void fireRemoved(int index) {
            fireIntervalRemoved(this, index, index);
        }
    }

    private class MockResponsesComboBoxModel extends AbstractListModel implements ComboBoxModel {
        public int getSize() {
            return getMockOperation().getMockResponseCount();
        }

        public Object getElementAt(int index) {
            return getMockOperation().getMockResponseAt(index).getName();
        }

        public void setSelectedItem(Object anItem) {
            Query query = getSelectedQuery();
            if (query != null) {
                query.setResponse(String.valueOf(anItem));
            }
        }

        public Object getSelectedItem() {
            Query query = getSelectedQuery();
            return query != null ? query.getResponse() : null;
        }
    }

    protected Query getSelectedQuery() {
        return queryDetailFormPresentationModel == null ? null : queryDetailFormPresentationModel.getBean();
    }

    private final class AddAction extends AbstractAction {
        public AddAction() {
            putValue(Action.SHORT_DESCRIPTION, "Adds a new Match");
            putValue(Action.SMALL_ICON, UISupport.createImageIcon("/add.png"));
        }

        public void actionPerformed(ActionEvent e) {
            String name = UISupport.prompt("Specify name for Match", "Add Query Match", "");
            if (name == null || name.trim().length() == 0) {
                return;
            }

            addQuery(name);
        }
    }

    private final class CopyAction extends AbstractAction {
        public CopyAction() {
            putValue(Action.SHORT_DESCRIPTION, "Copies the selected Match");
            putValue(Action.SMALL_ICON, UISupport.createImageIcon("/clone.png"));
        }

        public void actionPerformed(ActionEvent e) {
            QueryMatchMockOperationDispatcher.Query selectedQuery = getSelectedQuery();
            if (selectedQuery == null) {
                return;
            }

            String name = UISupport.prompt("Specify name for copied Query", "Copy Query", selectedQuery.getName());
            if (name == null || name.trim().length() == 0) {
                return;
            }

            QueryMatchMockOperationDispatcher.Query query = addQuery(name);
            query.setMatch(selectedQuery.getMatch());
            query.setQuery(selectedQuery.getQuery());
            query.setResponse(selectedQuery.getResponse());

            itemList.setSelectedIndex(getQueryCount() - 1);
        }
    }

    private final class DeleteAction extends AbstractAction {
        public DeleteAction() {
            putValue(Action.SMALL_ICON, UISupport.createImageIcon("/delete.png"));
            putValue(Action.SHORT_DESCRIPTION, "Deletes the selected Property Transfer");
        }

        public void actionPerformed(ActionEvent e) {
            QueryMatchMockOperationDispatcher.Query selectedQuery = getSelectedQuery();
            if (selectedQuery == null) {
                return;
            }

            if (UISupport.confirm("Delete selected Query", "Delete Query")) {
                deleteQuery(selectedQuery);
                if (getQueryCount() > 0) {
                    itemList.setSelectedIndex(0);
                }
            }
        }
    }

    private final class RenameAction extends AbstractAction {
        public RenameAction() {
            putValue(Action.SMALL_ICON, UISupport.createImageIcon("/rename.gif"));
            putValue(Action.SHORT_DESCRIPTION, "Renames the selected Property Transfer");
        }

        public void actionPerformed(ActionEvent e) {
            QueryMatchMockOperationDispatcher.Query selectedQuery = getSelectedQuery();
            if (selectedQuery == null) {
                return;
            }

            String newName = UISupport.prompt("Specify new name for Query", "Rename Query", selectedQuery.getName());

            if (newName != null && !selectedQuery.getName().equals(newName)) {
                selectedQuery.setName(newName);
                queryItemListModel.refresh();
            }
        }
    }

    private final class DeclareNamespacesAction extends AbstractAction {
        public DeclareNamespacesAction() {
            putValue(Action.SMALL_ICON, UISupport.createImageIcon("/declareNs.gif"));
            putValue(Action.SHORT_DESCRIPTION, "Declare request namespaces in current query");
        }

        public void actionPerformed(ActionEvent e) {
            QueryMatchMockOperationDispatcher.Query selectedQuery = getSelectedQuery();
            if (selectedQuery == null) {
                return;
            }

            try {
                MockResult lastResult = getMockOperation().getLastMockResult();
                String content = null;
                if (lastResult == null) {
                    if (!UISupport.confirm("Missing last result, declare from default request instead?",
                            "Declare Namespaces")) {
                        return;
                    }

                    content = getMockOperation().getOperation().createRequest(true);
                } else {
                    content = lastResult.getMockRequest().getRequestContent();
                }

                String path = selectedQuery.getQuery();
                if (path == null) {
                    path = "";
                }

                selectedQuery.setQuery(XmlUtils.declareXPathNamespaces(content) + path);
            } catch (Exception e1) {
                UISupport.showErrorMessage(e1);
            }
        }
    }

    private final class RunAction extends AbstractAction {
        public RunAction() {
            putValue(Action.SMALL_ICON, UISupport.createImageIcon("/run.png"));
            putValue(Action.SHORT_DESCRIPTION, "Runs Queries on last request");
        }

        public void actionPerformed(ActionEvent e) {
            MockResult result = getMockOperation().getLastMockResult();
            if (result != null) {
                try {
                    UISupport.showInfoMessage("Selected ["
                            + selectMockResponse(result.getMockRequest(), result).getName() + "]");
                } catch (DispatchException e1) {
                    UISupport.showErrorMessage(e1);
                }
            } else {
                UISupport.showErrorMessage("Missing request to query");
            }
        }
    }

    private final class ExtractFromCurrentAction extends AbstractAction {
        public ExtractFromCurrentAction() {
            super("Extract");
            putValue(Action.SHORT_DESCRIPTION, "Extracts the current value into the Value field");
        }

        public void actionPerformed(ActionEvent e) {
            QueryMatchMockOperationDispatcher.Query selectedQuery = getSelectedQuery();
            if (selectedQuery == null) {
                return;
            }

            MockResult result = getMockOperation().getLastMockResult();
            String content;

            if (result != null && StringUtils.hasContent(result.getMockRequest().getRequestContent())) {
                content = result.getMockRequest().getRequestContent();
            } else {
                if (!UISupport.confirm("Missing last result, extract from default request instead?", "Extract Match")) {
                    return;
                }

                content = getMockOperation().getOperation().createRequest(true);
            }

            XmlCursor cursor = null;

            try {
                // XmlObject xmlObject = XmlObject.Factory.parse( content );
                XmlObject xmlObject = XmlUtils.createXmlObject(content);
                cursor = xmlObject.newCursor();
                cursor.selectPath(selectedQuery.getQuery());
                if (!cursor.toNextSelection()) {
                    UISupport.showErrorMessage("Missing match in request");
                } else {
                    selectedQuery.setMatch(XmlUtils.getValueForMatch(cursor));
                }
            } catch (Throwable e1) {
                SoapUI.logError(e1);
            } finally {
                if (cursor != null) {
                    cursor.dispose();
                }
            }
        }
    }

    private class QueryItemListCellRenderer extends DefaultListCellRenderer {
        private Color defaultForeground;

        private QueryItemListCellRenderer() {
            this.defaultForeground = getForeground();
        }

        @Override
        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
                                                      boolean cellHasFocus) {
            JLabel component = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);

            Query query = (Query) value;
            component.setText(query.getName());
            component.setForeground(((Query) value).isDisabled() ? Color.GRAY : defaultForeground);

            return component;
        }
    }
}
